// astexplorer: https://astexplorer.net/
// babel-core doc: https://babeljs.io/docs/en/babel-core
import { transform } from 'sucrase';
import { babelParse } from '@vue/compiler-sfc';
import { createSFCModule } from './createVueSFCModule'

import {  formatErrorLineColumn, withCache, customTraverseAst } from "./utils"

import {
	Options,
	ModuleExport,
	Module,
	LoadingType,
	PathContext,
	AbstractPath,
	File,
} from './types'

/**
 * @internal
 */
export class Loading {

	promise : Promise<ModuleExport>;

	constructor(promise : Promise<ModuleExport>) {

		this.promise = promise;
	}
}



/**
 * @internal
 */
export async function transformJSCode(source : string, moduleSourceType : boolean, filename : AbstractPath, additionalBabelParserPlugins : Options['additionalBabelParserPlugins'], additionalBabelPlugins: Options['additionalBabelPlugins'], log : Options['log'], devMode: boolean) : Promise<[string[], string]> {
  const transformedScript = transform(source, {
    transforms: ['typescript','imports','jsx'],
    jsxRuntime: 'preserve',
    jsxImportSource: 'vue',
  })
  if (transformedScript.code) {
    let ast: any, depsList: string[] = [];
    try {
      ast = babelParse(transformedScript.code, {
        sourceFilename: filename.toString(),
        sourceType: moduleSourceType ? 'module' : 'script',
        plugins:[
          'typescript','jsx',
          ...additionalBabelParserPlugins !== undefined ? additionalBabelParserPlugins : [],
        ]
      })
      const res = customTraverseAst(ast)
      depsList = res.depsList
      // if (res.hasJsx || res.hasTsx){
      //   const { transformAst } = await import("./babel")
      //   const result: any = await transformAst(ast, transformedScript.code, moduleSourceType, additionalBabelPlugins, devMode, log)
      //   transformedScript.code = result.code
      // }
    } catch(ex) {
      log('error', 'parse script', formatErrorLineColumn(ex.message, filename.toString(), source, ex?.loc?.line || 0, (ex?.loc?.column + 1)  || 0) );
      throw ex;
    }
    return [ depsList, transformedScript.code ];
  }else{
    const msg = `unable to transform script "${filename.toString()}"`;
    log?.('error', msg);
    throw new Error(msg)
  }
}

// module tools
export async function loadModuleInternal(pathCx : PathContext, options : Options) : Promise<ModuleExport> {

	const { moduleCache, loadModule, handleModule } = options;

	const { id, path, getContent } = options.getResource(pathCx, options);

	if ( id in moduleCache ) {

		if ( moduleCache[id] instanceof Loading )
			return await (moduleCache[id] as Loading).promise;
		else
			return moduleCache[id];
	}


	moduleCache[id] = new Loading((async () => {

		// note: null module is accepted
		let module : ModuleExport | undefined | null = undefined;

		if ( loadModule )
			module = await loadModule(id, options);

		if ( module === undefined ) {

			const { getContentData, type } = await getContent();

			if ( handleModule !== undefined )
				module = await handleModule(type, getContentData, path, options);

			if ( module === undefined )
				module = await handleModuleInternal(type, getContentData, path, options);

			if ( module === undefined )
				throw new TypeError(`Unable to handle ${ type } files (${ path })`);
		}

		return moduleCache[id] = module;

	})());

	return await (moduleCache[id] as LoadingType<ModuleExport>).promise;
}

/**
 * Create a cjs module
 * @internal
 */
export function defaultCreateCJSModule(refPath : AbstractPath, source : string, options : Options) : Module {

	const { moduleCache, pathResolve, getResource } = options;

	const require = function(relPath : string) {

		const { id } = getResource({ refPath, relPath }, options);
		if ( id in moduleCache )
			return moduleCache[id];

		throw new Error(`require(${ JSON.stringify(id) }) failed. module not found in moduleCache`);
	}

	const importFunction = async function(relPath : string) {
		return await loadModuleInternal({ refPath, relPath }, options);
	}

	const module = {
		exports: {}
	}

  // see https://github.com/nodejs/node/blob/a46b21f556a83e43965897088778ddc7d46019ae/lib/internal/modules/cjs/loader.js#L195-L198
  // see https://github.com/nodejs/node/blob/a46b21f556a83e43965897088778ddc7d46019ae/lib/internal/modules/cjs/loader.js#L1102
  const moduleFunction = Function('exports', 'require', 'module', '__filename', '__dirname', '__vsfcl_import__', source);
  moduleFunction.call(module.exports, module.exports, require, module, refPath, pathResolve({ refPath, relPath: '.' }, options), importFunction);

	return module;
}


/**
 * @internal
 */
export async function createJSModule(source : string, moduleSourceType : boolean, filename : AbstractPath, options : Options) : Promise<ModuleExport> {

	const { compiledCache, additionalBabelParserPlugins, additionalBabelPlugins, createCJSModule, log } = options;

	const [depsList, transformedSource] =
		await withCache(
			compiledCache,
			[
				source,
				filename,
				options.devMode,
				additionalBabelParserPlugins ? additionalBabelParserPlugins : '',
				additionalBabelPlugins ? Object.keys(additionalBabelPlugins) : '',
			],
			async () => {

		return await transformJSCode(source, moduleSourceType, filename, additionalBabelParserPlugins, additionalBabelPlugins, log, options.devMode);
	});

	await loadDeps(filename, depsList, options);
	return createCJSModule(filename, transformedSource, options).exports;
}


/**
 * Just load and cache given dependencies.
 * @internal
 */
export async function loadDeps(refPath : AbstractPath, deps : AbstractPath[], options : Options) : Promise<void> {

	await Promise.all(deps.map(relPath => loadModuleInternal({ refPath, relPath }, options)))
}


/**
 * Default implementation of handleModule
 */
 async function handleModuleInternal(type : string, getContentData : File['getContentData'], path : AbstractPath, options : Options) : Promise<ModuleExport | undefined> {

	switch (type) {
		case '.vue': return createSFCModule((await getContentData(false)) as string, path, options);
		case '.js': return createJSModule((await getContentData(false)) as string, false, path, options);
		case '.mjs': return createJSModule((await getContentData(false)) as string, true, path, options);
		case '.ts': return createJSModule((await getContentData(false)) as string, true, path, {
			...options,
		});
	}

	return undefined;
}
